/**
 * Copyright (c), 上海沃恩信息科技有限公司.
 * All rights reserved.
 * Licensed under BSD (https://www.pittypat.work/bsd.html).
 */

package pittypat.web;

import java.beans.PropertyVetoException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URISyntaxException;
import java.sql.SQLException;
import java.util.List;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.websocket.EndpointConfig;
import javax.websocket.Session;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;

import pittypat.AuthorityService;
import pittypat.IAuthorityService;
import pittypat.IUser;
import pittypat.Permission;
import pittypat.SystemTokenProvider;
import pittypat.User;
import pittypat.configuration.ConfigException;

/**
 * 向目标功能请求处理程序提供客户端地址、首选语言、用户信息、功能信息等。
 */
public final class RequestContext {
	// 在 WebSocket Session 或 HttpServletRequest 中保存 RequestContext 对象的键值名称
	private static final String STORAGE_NAME = "__REQUEST_CONTEXT";

	// Accept-Language 标头名称
	private static final String ACCEPT_LANGUAGE_HEADER_NAME = "Accept-Language";

	/**
	 * 位于表单字段或 COOKIE 中用户令牌的字段名称。这是静态只读字段，值为 "__USER_TOKEN"。
	 */
	public static final String USER_TOKEN_NAME = "__USER_TOKEN";

	/**
	 * 当前用户在请求中的保存名称。这是静态只读字段，值为 "__USER"。
	 */
	public static final String USER_ATTRIBUTE_NAME = "__USER";

	// 当前功能的信息。
	private FunctionInfo funcInfo;
	// 对用户进行权限检查的结果
	private Permission permission;
	// 当前用户的用户令牌
	private String userToken;
	// 当前请求路径
	private String[] pathParts;

	/**
	 * 使用请求的目标功能的信息初始化 RequestContext 类的新实例。
	 * 
	 * @param funcInfo
	 *            功能信息。
	 * @param permission
	 *            用户权限。可能为 null，此时认为是匿名用户 Permission.ANONYMOUS_UNAUTHORIZED。
	 * @param userToken
	 *            用户令牌。
	 */
	private RequestContext(FunctionInfo funcInfo, Permission permission, String userToken, String[] pathParts) {
		this.funcInfo = funcInfo;
		this.permission = permission == null ? Permission.ANONYMOUS_UNAUTHORIZED : permission;
		this.userToken = userToken == null ? "" : userToken;
		this.pathParts = pathParts;
	}

	/**
	 * 获取当前请求路径的各个部分。
	 * <p>
	 * 第一个元素是去掉 appPath 后的完整路径，其他分别为：开发者代码、App 标识符、模块标识符和功能标识符。 如果是 /user/* 或
	 * /notification/*，则返回的数组只有三个元素
	 * 
	 * @return 一个字符串数组。
	 */
	public String[] getPathParts() {
		return this.pathParts;
	}

	/**
	 * 获取当前用户的用户令牌。
	 * 
	 * @return 用户令牌。对匿名用户返回空字符串。
	 */
	public String getUserToken() {
		return this.userToken;
	}

	/**
	 * 获取当前用户的权限。
	 * 
	 * @return 一个 Permission 对象。
	 */
	public Permission getPermission() {
		return this.permission;
	}

	/**
	 * 获取当前用户信息。
	 * 
	 * @return 一个 IUser 对象。
	 */
	public IUser getUser() {
		IUser user = this.permission == null ? null : this.permission.getUser();
		return user == null ? User.ANONYMOUS : user;
	}

	/**
	 * 获取当前请求的功能的信息。
	 * 
	 * @return 一个 FunctionInfo 对象，可能为 null。
	 */
	public FunctionInfo getFuncInfo() {
		return this.funcInfo;
	}

	/**
	 * 从一个 HttpServletRequest 中获取此前已经保存的 RequestContext。
	 * 
	 * @param request
	 *            一个 HttpServletRequest。
	 * @return 一个 RequestContext。
	 */
	public static RequestContext getRequestContext(HttpServletRequest request) {
		return request == null ? null : (RequestContext) request.getAttribute(STORAGE_NAME);
	}

	/**
	 * 为指定的 HttpServletRequest 请求设置用户信息。
	 * 
	 * @param request
	 *            目标请求。
	 * @return 新创建的 RequestContext。
	 * @throws URISyntaxException
	 * @throws InvocationTargetException
	 * @throws IllegalArgumentException
	 * @throws SecurityException
	 * @throws NoSuchFieldException
	 * @throws IllegalAccessException
	 * @throws InstantiationException
	 * @throws ClassNotFoundException
	 * @throws PropertyVetoException
	 * @throws SQLException
	 * @throws ConfigException
	 * @throws NoSuchMethodException
	 * @throws UnsupportedEncodingException
	 */
	public static RequestContext setupForServlet(HttpServletRequest request)
			throws URISyntaxException, ClassNotFoundException, InstantiationException, IllegalAccessException,
			NoSuchFieldException, SecurityException, IllegalArgumentException, InvocationTargetException,
			UnsupportedEncodingException, NoSuchMethodException, ConfigException, SQLException, PropertyVetoException {
		FunctionInfo funcInfo = null;
		Permission permission = null;

		// 从请求中解析用户令牌
		String userToken = parseUserToken(request);

		// 获取请求路径，第一个元素是去掉 appPath 后的完整路径，其他分别为：开发者代码、App 标识符、模块标识符和功能标识符。
		// 如果是 /user/* 或 /notification/*，则返回的数组只有三个元素
		String[] pathParts = Path.getFuncPaths(new URI(request.getRequestURI()));

		if (pathParts != null && pathParts.length > 0) {
			// 提供了有效的功能路径
			if (!"user".equalsIgnoreCase(pathParts[1]) && !"notification".equalsIgnoreCase(pathParts[1])) {
				// 不是与用户或通知相关的请求，与用户或通知相关的请求无法获知请求功能，并且用户也可能是匿名用户，但是可能需要用户令牌
				// 获取请求对应的功能信息
				funcInfo = FunctionInfo.create(pathParts, true /* createModel */);
				if (funcInfo != null) {
					// 如果正在上传文件，那么 funcModel 为 null
					if (funcInfo.funcModel != null) {
						// 环境变量参数列表
						funcInfo.funcModel.setContext(FunctionInfo.createFunctionContext(
								request.getHeader(ACCEPT_LANGUAGE_HEADER_NAME), request.getRemoteAddr()));
					}

					if (!funcInfo.moduleInfo.isMaintenance) {
						// 指示目标模块正在维护当中，此时不去耗费资源查询用户信息和检查权限，在 ApiServlet 或
						// WebSocket
						// 中要首先检查这种情况。
						// 检查用户的权限
						permission = funcInfo.demand(userToken);
					}
				}
			}
		}

		// 创建 RequestContext 对象实例
		RequestContext context = new RequestContext(funcInfo, permission, userToken, pathParts);

		// 保存到 request 中
		request.setAttribute(STORAGE_NAME, context);

		return context;
	}

	/**
	 * 将 RequestContext 从一个 EndpointConfig 复制到 Session。
	 * 
	 * @param config
	 *            一个 EndpointConfig。
	 * @param session
	 *            一个 Session。
	 * @return 一个 RequestContext。
	 */
	public static RequestContext copy(EndpointConfig config, Session session) {
		RequestContext context = config == null ? null : (RequestContext) config.getUserProperties().get(STORAGE_NAME);
		if (session != null) {
			session.getUserProperties().put(STORAGE_NAME, context);
		}

		return context;
	}

	/**
	 * 从一个 Session 中获取此前已经保存的 RequestContext。
	 * 
	 * @param session
	 *            一个 Session。
	 * @return 一个 RequestContext。
	 */
	public static RequestContext getRequestContext(Session session) {
		return session == null ? null : (RequestContext) session.getUserProperties().get(STORAGE_NAME);
	}

	// 判断是否正在尝试上传文件
	private static boolean isUploadFiles(HandshakeRequest request) {
		List<String> list = request.getHeaders().get(HandshakeRequest.SEC_WEBSOCKET_PROTOCOL);

		if (list == null || list.size() == 0) {
			return false;
		}

		for (String p : list) {
			if (UploadSocket.UPLOAD_SUB_PROTOCOL.equalsIgnoreCase(p)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * 为指定的 HandshakeRequest 请求设置用户信息。
	 * 
	 * @param request
	 *            目标请求。
	 * @param config
	 *            服务器端点配置。
	 * @return 新创建的 RequestContext。
	 * @throws InvocationTargetException
	 * @throws IllegalArgumentException
	 * @throws SecurityException
	 * @throws NoSuchFieldException
	 * @throws IllegalAccessException
	 * @throws InstantiationException
	 * @throws ClassNotFoundException
	 * @throws PropertyVetoException
	 * @throws SQLException
	 * @throws ConfigException
	 * @throws NoSuchMethodException
	 * @throws UnsupportedEncodingException
	 */
	public static RequestContext setupForWebSocket(HandshakeRequest request, ServerEndpointConfig config)
			throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException,
			SecurityException, IllegalArgumentException, InvocationTargetException, UnsupportedEncodingException,
			NoSuchMethodException, ConfigException, SQLException, PropertyVetoException {
		FunctionInfo funcInfo = null;
		Permission permission = null;

		// 从请求中解析用户令牌
		String userToken = parseUserToken(request);

		// 获取请求路径，第一个元素是去掉 appPath 后的完整路径，其他分别为：开发者代码、App 标识符、模块标识符和功能标识符。
		// 如果是 /user/* 或 /notification/*，则返回的数组只有三个元素
		String[] pathParts = Path.getFuncPaths(request.getRequestURI());

		if (pathParts != null && pathParts.length > 0) {
			// 提供了有效的功能路径
			if (!"user".equalsIgnoreCase(pathParts[1]) && !"notification".equalsIgnoreCase(pathParts[1])) {
				// 不是与用户或通知相关的请求，与用户或通知相关的请求无法获知请求功能，并且用户也可能是匿名用户，但是可能需要用户令牌
				// 获取请求对应的功能信息
				funcInfo = FunctionInfo.create(pathParts, !isUploadFiles(request) /* createModel */);
				if (funcInfo != null) {
					// 如果正在上传文件，那么 funcModel 为 null
					if (funcInfo.funcModel != null) {
						// 环境变量参数列表
						List<String> langs = request.getHeaders().get(ACCEPT_LANGUAGE_HEADER_NAME);
						funcInfo.funcModel.setContext(FunctionInfo.createFunctionContext(
								langs == null || langs.size() == 0 ? "" : langs.get(0), /* 首选语言 */
								"" /* 无法从 HandshakeRequest 获得客户端地址 */));
					}

					if (!funcInfo.moduleInfo.isMaintenance) {
						// 指示目标模块正在维护当中，此时不去耗费资源查询用户信息和检查权限，在 ApiServlet 或
						// WebSocket
						// 中要首先检查这种情况。
						// 检查用户的权限
						permission = funcInfo.demand(userToken);
					}
				}
			}
		}

		// 创建 RequestContext 对象实例
		RequestContext context = new RequestContext(funcInfo, permission, userToken, pathParts);

		// 保存到 config 中
		config.getUserProperties().put(STORAGE_NAME, context);

		return context;
	}

	/**
	 * 从请求中获取当前用户的用户令牌。
	 * <p>
	 * 如果客户端启用了 COOKIE，则用户令牌将通过表单和 COOKIE 发送到服务器，必须合并这两部分才能得到完整的令牌。 如果客户端没有启用
	 * COOKIE，则用户令牌将仅仅通过表单发送到服务器。
	 * 
	 * @param request
	 *            当前请求。
	 * @return 用户令牌，如果没有用户令牌，或者用户令牌无效，则返回空字符串。
	 */
	private static String parseUserToken(HttpServletRequest request) {
		// 首先是表单字段部分
		String formPart = request.getParameter(USER_TOKEN_NAME);
		if (formPart == null || formPart.length() == 0) {
			return "";
		}

		// COOKIE 部分
		Cookie[] cookies = request.getCookies();
		if (cookies == null || cookies.length == 0) {
			// 没有任何 COOKIE，仅仅将表单部分返回
			return formPart;
		}

		// 查找 COOKIE 部分
		String cookiePart = "";
		for (Cookie c : cookies) {
			if (USER_TOKEN_NAME.equalsIgnoreCase(c.getName())) {
				cookiePart = c.getValue();
				break;
			}
		}

		// 合并表单部分和 COOKIE 部分
		return cookiePart == null || cookiePart.length() == 0 ? formPart : (formPart + cookiePart);
	}

	/**
	 * 从请求中获取当前用户的用户令牌。
	 * <p>
	 * 如果客户端启用了 COOKIE，则用户令牌将通过表单和 COOKIE 发送到服务器，必须合并这两部分才能得到完整的令牌。 如果客户端没有启用
	 * COOKIE，则用户令牌将仅仅通过表单发送到服务器。
	 * 
	 * @param request
	 *            当前请求。
	 * @return 用户令牌，如果没有用户令牌，或者用户令牌无效，则返回空字符串。
	 */
	private static String parseUserToken(HandshakeRequest request) {
		List<String> values = request.getParameterMap().get(USER_TOKEN_NAME);

		// 首先是表单字段部分
		String formPart = values == null || values.size() == 0 ? null : values.get(0);
		if (formPart == null || formPart.length() == 0) {
			return "";
		}

		// COOKIE 部分
		values = request.getHeaders().get("Cookie");
		if (values == null || values.size() == 0) {
			// 没有任何 COOKIE，仅仅将表单部分返回
			return formPart;
		}

		// 查找 COOKIE 部分
		String cookie = "";
		for (String value : values) {
			if (value != null && value.length() > 0) {
				cookie = value;
				break;
			}
		}

		// 没有提供 COOKIE
		if (cookie == null || cookie.length() == 0) {
			return formPart;
		}

		int start = cookie.indexOf(USER_TOKEN_NAME + "=");
		if (start < 0 || start >= cookie.length()) {
			return formPart;
		}

		// 将位置移动到 '=' 之后
		start += USER_TOKEN_NAME.length() + 1;

		// 查找值的结束位置
		int end = cookie.indexOf(';', start);
		if (end < start) {
			end = cookie.indexOf(',', start);

			if (end < start) {
				end = cookie.length();
			}
		}

		if (end <= start) {
			return formPart;
		}

		String cookiePart = cookie.substring(start, end);

		// 合并表单部分和 COOKIE 部分
		return cookiePart == null || cookiePart.length() == 0 ? formPart : (formPart + cookiePart);
	}

	/**
	 * 根据用户令牌加载用户信息，不进行权限检查。对 /notification/* 的请求将使用此方法获取用户资料。
	 * 
	 * @param userToken
	 *            目标用户令牌。
	 * @return 如果用户已经登录，返回目标用户的用户信息，如果尚未登录或者已经过期，则返回一个匿名用户。
	 * @throws IllegalAccessException
	 * @throws InstantiationException
	 * @throws ClassNotFoundException
	 * @throws PropertyVetoException
	 * @throws ConfigException
	 * @throws SQLException
	 * @throws SecurityException
	 * @throws NoSuchMethodException
	 * @throws InvocationTargetException
	 * @throws UnsupportedEncodingException
	 * @throws IllegalArgumentException
	 */
	public static IUser getUser(String userToken) throws ClassNotFoundException, InstantiationException,
			IllegalAccessException, IllegalArgumentException, UnsupportedEncodingException, InvocationTargetException,
			NoSuchMethodException, SecurityException, SQLException, ConfigException, PropertyVetoException {
		if (userToken == null || userToken.length() == 0) {
			// 没有提供用户令牌，认为当前用户是匿名用户
			return User.ANONYMOUS;
		}

		// 获取当前验证服务
		IAuthorityService authService = AuthorityService.getCurrent();

		if (authService == null) {
			// 没有提供验证服务
			throw new IllegalStateException("没有配置用户权限验证安全服务，请检查配置文件的 authenticationService 配置项目。");
		}

		// 传递空的功能路径，获取用户信息
		Permission permission = authService.demand(SystemTokenProvider.getCurrent().getSystemToken(), userToken,
				"" /* funcPath */, WebAsyncProgress.DEFAULT);
		IUser user = permission == null ? null : permission.getUser();

		return user == null || user.getId() == null ? User.ANONYMOUS : user;
	}
}
